package com.lins.storedisk.bo.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import com.lins.storedisk.bo.FileBo;
import com.lins.storedisk.common.APIException;
import com.lins.storedisk.common.AppCode;
import com.lins.storedisk.constant.MyEnum;
import com.lins.storedisk.constant.SysParams;
import com.lins.storedisk.dao.FileDao;
import com.lins.storedisk.dao.UserSetDao;
import com.lins.storedisk.dto.FileDTO;
import com.lins.storedisk.dto.SearchDTO;
import com.lins.storedisk.dto.TreeDTO;
import com.lins.storedisk.entity.FileEntity;
import com.lins.storedisk.entity.UserSetEntity;
import com.lins.storedisk.oss.OssUtil;
import com.lins.storedisk.utils.RedisUtil;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * @Author linshuo
 * @Date 2023/12/4 13:11
 */
@Slf4j
@Service
public class FileBoImpl implements FileBo {
    @Autowired
    SysParams params;
    @Autowired
    FileDao fileDao;
    @Autowired
    UserSetDao setDao;
    @Autowired
    OssUtil ossUtil;
    @Autowired
    RedisUtil redisUtil;

    private List<FileEntity> fileEntityList;

    private JPAQueryFactory queryFactory;

    @PostConstruct
    public void init(){
        queryFactory = new JPAQueryFactory(entityManager);
    }

    @PersistenceContext //持久化上下文，⾥⾯管理的是 Entity 的状态，EntityManager 是通过 PersistenceContext 创建的，⽤来管理 PersistenceContext 中 Entity 状态的⽅法，离开 PersistenceContext 持久化上下⽂，EntityManager 没有意义
    private EntityManager entityManager; //操作对象的唯一入口

    @Override
    public List<String> upload(MultipartFile[] multipartFiles, String parUrl) {
        if (!StringUtils.hasText(parUrl)) parUrl = "/home";
        List<String> result = new ArrayList<>();
        for (MultipartFile mf : multipartFiles) {
            if (mf.isEmpty()) throw new APIException(AppCode.SaveFAIL,"文件可能是空的");

            String filename = mf.getOriginalFilename();
            if (!StringUtils.hasText(filename))throw new APIException(AppCode.SaveFAIL,"文件名称不允许为空");

            //检验同级目录下 文件名是否重复 重复则返回新名
            filename = checkRepeat(MyEnum.FILE,filename,parUrl,"文件名重复！");

            String url = parUrl +"/"+filename;
            String floderUrl = System.getProperty("user.dir")//项目工作目录(绝对地址)
                    + params.getLocalFileUrl() + parUrl;
            File folder = new File(floderUrl);
            if (!folder.isDirectory()){
                //目录不存在则创建
                folder.mkdirs();
            }

            //本地转存文件
            File file = new File(folder, filename);
            CompletableFuture.runAsync(()->{
                try {
                    mf.transferTo(file);
                } catch (IOException e) {
                    throw new APIException(AppCode.SaveFAIL,"本地转存文件执行异常");
                }
            });

            //OSS转存文件
//            CompletableFuture.runAsync(()-> ossUtil.upload(url,mf,file));
            ossUtil.upload(url,mf,file);

            //保存文件信息
            FileEntity fileEntity = new FileEntity(filename,computeFileSize(mf.getSize()),
                    MyEnum.NOT_SHARE.getValue(),url,MyEnum.FILE.getValue(),parUrl,MyEnum.STATE.getValue(),Integer.valueOf(StpUtil.getLoginId().toString()));

            //查询父文件夹id
            FileEntity floder = fileDao.findByUrlAndFileType(parUrl, MyEnum.FLODER.getValue());
            fileEntity.setParId(null==floder?0:floder.getId());

            fileDao.save(fileEntity);
            result.add(url);
        }
        return result;
    }

    @Override
    public void download(Integer fid, HttpServletResponse response) {
        if (null == fid || 0 == fid) throw new APIException(AppCode.PARAM_ERROR,"文件id为空或为0不合法");

        FileEntity fileEntity = fileDao.findByIdAndUid(fid, Integer.valueOf(StpUtil.getLoginId().toString()));
        if (null == fileEntity) throw new APIException(AppCode.FILESTATE_ERROR,"目标文件为空!或文件持有人未开启共享权限！");

        String fileName = fileEntity.getFileName();
        String fileUrl = fileEntity.getUrl();
        String fileLocalPath = System.getProperty("user.dir") //项目工作目录(绝对地址)
                + params.getLocalFileUrl() + fileUrl;

        response.reset();//重置
        response.setContentType("application/octet-stream");//告知客户端文件类型
        //告知客户端以下载方式处理响应数据 指定下载文件的文件名
        try {
            response.setHeader("Content-Disposition","attachment; filename=" + new String((fileName).getBytes("GBK"),
                            "ISO8859-1"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        if (!downloadLocal(fileLocalPath,response)){
            ossUtil.download(fileUrl,fileLocalPath); //若本地文件不存在，从OSS拉取
            if (!downloadLocal(fileLocalPath,response)){  //还未成功，抛异常
                throw new APIException(AppCode.FILEIO_ERROR,"下载文件发生异常");
            }
        }

    }

    @Override
    public List<FileDTO> findList(SearchDTO dto) {
//        JPAQuery<FileEntity> from = queryFactory.select(QFileEntity.fileEntity).from(QFileEntity.fileEntity);
//        queryFactory.selectFrom(QFileEntity.fileEntity).where()

        Integer uid = Integer.valueOf(StpUtil.getLoginId().toString());
        if (!StringUtils.hasText(dto.getParUrl())) throw new APIException(AppCode.PARAM_ERROR,"没有文件夹URL");

        //基本条件
        FileEntity file = new FileEntity();
        file.setUid(uid);
        file.setParUrl(dto.getParUrl());
        file.setState(MyEnum.STATE.getValue());
        //是否共享
//        file.setShare(MyEnum.IS_SHARE.getValue());

        boolean havaKeyword = false;
        if (StringUtils.hasText(dto.getKeyword())){
            file.setFileName(dto.getKeyword());  //todo 这里是like
            havaKeyword = true;
        }

        Example<FileEntity> example = Example.of(file);
        List<FileEntity> list = fileDao.findAll(example);

        if (havaKeyword && dto.getSelectZi()){
            dto.setUid(uid); //如果用户没传uid
            //检索子文件夹
            list = selectZi(dto); //覆盖
        }

        return list.size() == 0 ? nullData(false) : fileEntity2DTO(list);
    }


    //检索子文件夹
    private List<FileEntity> selectZi(SearchDTO dto){
        //获取该用户目录下所有正常状态的文件，包括文件夹
        List<FileEntity> list = fileDao.findByUidAndParUrlAndState(dto.getUid(), dto.getParUrl(),MyEnum.STATE.getValue());

        List<FileEntity> compute = new ArrayList<>();
        getDirectory(list,compute); //获取文件夹数据 并保存

        return compute.stream().filter(e ->
                        e.getFileType().equals(MyEnum.FILE.getValue())
//                                && e.getFileName().equals(dto.getKeyword()))
                                && e.getUrl().equals(dto.getKeyword())) //联想已经模糊搜索了，所以这里根据url匹配
                .collect(Collectors.toList());
    }

    /**
     * 递归查找
     **/
    private void getDirectory(List<FileEntity> list,List<FileEntity> compute){
        if (list == null || list.size() == 0) return;
//        redisUtil.setCacheObject("fileTemp:"+"selectZi", list);
        compute.addAll(list);  //当前层所有文件保存
        //循环判断 当前层所有文件是否属于文件夹
        for (FileEntity f:list) {
            //属于文件夹则继续根据文件夹id循环查找
            if (MyEnum.FLODER.getValue().equals(f.getFileType())){
                //查出当前文件夹的 所有子文件
                List<FileEntity> ele = fileDao.findByUidAndParIdAndState(f.getUid(),f.getId(),MyEnum.STATE.getValue());
                getDirectory(ele,compute); //结果再次判断，直至没有内容或没有文件夹
            }
        }
    }

    @Override
    public Integer delete(Integer fid) {
        if (null == fid) throw new APIException(AppCode.PARAM_ERROR,"文件id不允许为空");
        //没有删除文件，所以删除后访问原文件url可以继续查看和下载，只是对于页面没有了入口
        fileDao.deleteById(fid);
        return 1;
    }

    @Override
    public Integer saveFloder(String floderName,String parUrl) {
        if (!StringUtils.hasText(floderName)) throw new APIException(AppCode.SaveFAIL,"文件夹名称不允许为空");

        //文件夹不允许有. 防止logo匹配
        if (floderName.contains(".")) floderName = floderName.replace(".","_");

        //检验同级目录下 文件名是否重复 重复返回新名
        floderName = checkRepeat(MyEnum.FLODER,floderName,parUrl, "新建文件夹名称在当前目录已存在！");

        //准备保存
        FileEntity file = new FileEntity(floderName,"...",MyEnum.NOT_SHARE.getValue(),parUrl+"/"+floderName,
                MyEnum.FLODER.getValue(),parUrl,MyEnum.STATE.getValue(),Integer.valueOf(StpUtil.getLoginId().toString()));
        /**
         * 常规情况下，应该传入父级id再获取父级url，这里暂用parUrl查询父文件夹id
         **/
        FileEntity floder = fileDao.findByUrlAndFileType(parUrl, MyEnum.FLODER.getValue());
        file.setParId(null==floder?0:floder.getId());
        FileEntity save = fileDao.save(file);
        return save.getId();
    }

    @Override
    public Integer rename(Integer fid, String newName) {
        FileEntity file = fileDao.findById(fid).get();
        String oldName = file.getFileName();
        String oldUrl = file.getUrl();

        boolean isFile = MyEnum.FILE.getValue().equals(file.getFileType());
        if (isFile){
            int index = oldName.lastIndexOf(".");
            if (index < 0) throw new APIException(AppCode.SaveFAIL,"旧文件名格式有误");
            String suffix = oldName.substring(index); //文件后缀
            newName += suffix;
        }

        if (oldName.equals(newName)) throw new APIException(AppCode.SaveFAIL,"新旧名称一致，请核对后再试");
        file.setFileName(newName);

        String parUrl = file.getParUrl();
        String newUrl = parUrl+"/"+newName;
        file.setUrl(newUrl);

        if (isFile){
            this.renameTo(oldUrl,newUrl,"重命名");
            ossUtil.rename(oldUrl,newUrl);
        }
        fileDao.save(file);
        return 1;
    }

    @Override
    public List<TreeDTO> findTree() {
        List<TreeDTO> res = new LinkedList<>();
        List<FileEntity> list = fileDao.findByFileTypeAndUid(MyEnum.FLODER.getValue(), Integer.valueOf(StpUtil.getLoginId().toString()));
        list.forEach(f ->res.add(new TreeDTO(f.getId(),f.getParId(),f.getFileName())));
        return res;
    }

    @Override
    public Integer move(Integer fid,Integer pid) {
        FileEntity parFile = fileDao.findById(pid).get();
        String parUrl = parFile.getUrl();

        FileEntity file = fileDao.findById(fid).get();
        String oldUrl = file.getUrl();
        String newUrl = parUrl+"/"+file.getFileName();

        try {
            this.moveTo(oldUrl,newUrl);
        } catch (Exception e) {
            this.renameTo(oldUrl,newUrl,"移动");
        }
        ossUtil.rename(oldUrl,newUrl);

        file.setParId(pid);
        file.setParUrl(parUrl);
        file.setUrl(newUrl);
        fileDao.save(file);

        return 1;
    }

    @Override
    public Integer changeShare(Integer fid,Integer uid) {
        if (uid != Integer.valueOf(StpUtil.getLoginId().toString()))
            throw new APIException(AppCode.USERINFO_ERROR,"参数uid与token对应用户不一致");

        FileEntity file = fileDao.findById(fid).get();
        String share = file.getShare();
        if (!StringUtils.hasText(share)) throw new APIException(AppCode.FILESTATE_ERROR,"请联系管理员");

        String is = MyEnum.IS_SHARE.getValue();
        String not = MyEnum.NOT_SHARE.getValue();

        file.setShare(share.equals(is)?not:is);
        fileDao.save(file);
        return 1;
    }

    @Override
    public List<FileEntity> searchLike(String keyword) {
         return fileDao.findByUidAndFileTypeAndFileNameContaining(
                 Integer.valueOf(StpUtil.getLoginId().toString()),
                 MyEnum.FILE.getValue(),keyword);
    }

    private void renameTo(String oldUrl, String newUrl, String hint){
        String path = System.getProperty("user.dir") + params.getLocalFileUrl();
        String oldPath = path + oldUrl;
        String newPath = path + newUrl;

        File oldFile = new File(oldPath);
        File newFile = new File(newPath);

        if (!oldFile.exists()) throw new APIException(AppCode.FILEIO_ERROR,"源文件在本地不存在");
        if (!oldFile.renameTo(newFile)) throw new APIException(AppCode.FILEIO_ERROR,hint+"文件失败");
    }

    private void moveTo(String oldUrl,String newUrl){
        String path = System.getProperty("user.dir") + params.getLocalFileUrl();
        Path oldPath = Paths.get(path + oldUrl);
        Path newPath = Paths.get(path + newUrl);

        if (!Files.exists(newPath)){
            try {
                Files.createDirectories(newPath);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        try {
            Path move = Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }



    }

    private boolean downloadLocal(String fileLocalPath, HttpServletResponse response){
        try {
            File file = new File(fileLocalPath);
            //InputStream inputStream = new FileInputStream(file);
            InputStream inputStream = Files.newInputStream(file.toPath());
            IOUtils.copy(inputStream, response.getOutputStream()); //将输入流拷贝到输出流
            response.flushBuffer(); //响应刷新缓存 代表执行刷出
            inputStream.close(); //关闭流资源
            return true;
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return false;
    }


    /**
     * 文件实体转DTO
     **/
    private List<FileDTO> fileEntity2DTO(List<FileEntity> entities){
        List<FileDTO> rList = new ArrayList<>();
        entities.forEach(e -> {
            FileDTO dto = new FileDTO();
            BeanUtils.copyProperties(e,dto);
            dto.setCreateTimeFormat(e.getCreatedTime());
            //加图片名
            int index = e.getFileName().lastIndexOf(".");
            dto.setImgUrl(index < 0?"floder.png":e.getFileName().substring(index + 1)+".png");
            rList.add(dto);
        });
        return rList;
    }

    /**
     * 处理空列表时的假数据
     * @Param:   b true带内容，false不带
     **/
    private List<FileDTO> nullData(boolean b){
        if (!b) return null; //不要内容返回空
        String p = "……";
        FileDTO nullDto = new FileDTO(p,p,p,p,MyEnum.FILE.getValue(),p,MyEnum.NOT_SHARE.getValue(),0,"..png",p);
        ArrayList<FileDTO> nullData = new ArrayList<>();
        nullData.add(nullDto);
        return nullData;
    }

    /**
     * 计算文件大小
     **/
    private String computeFileSize(long size){
        String r;
        DecimalFormat df = new DecimalFormat("0.00");
        if (size < 1024) {
            r = df.format((double) size) + "字节";
        } else if (size < 1048576) {
            r = df.format((double) size / 1024) + "KB";
        } else if (size < 1073741824) {
            r = df.format((double) size / 1048576) + "MB";
        } else {
            r = df.format((double) size / 1073741824) + "GB";
        }
        return r;
    }

    private String pid2parUrl(Integer pid){
        //根据pid作为fid拿来url作为parUrl
        FileEntity file = fileDao.findById(pid).get();
        return null==file?"/home":file.getUrl();
    }

    /**
     * 检验同级目录下 文件名是否重复
     */
    private boolean isExistFilename(String filename,String p){
        this.fileEntityList = fileDao.findByUidAndFileNameAndParUrlAndState(
                Integer.valueOf(StpUtil.getLoginId().toString()),
                filename,
                p.contains("/") ? p : pid2parUrl(Integer.valueOf(p)),
                MyEnum.STATE.getValue()
        );
        return fileEntityList.size()>0?true:false;
    }

    private String fileRename(String filename,Date date){
        if (filename.contains(".")){
            int lastPointIndex = filename.lastIndexOf(".");
            String realName = filename.substring(0,lastPointIndex);
            String suffix = filename.substring(lastPointIndex,filename.length());
            filename = realName+DateUtil.format(date, "yyyyMMddHHmmss")+suffix;
        }else {
            filename = filename+DateUtil.format(date, "yyyyMMddHHmmss");
        }
        return filename;
    }

    private String fileRename(String filename,int i,String p){ //order
        String original = filename; //最初的文件名
        int lastPointIndex = filename.lastIndexOf(".");
        if (lastPointIndex<0){ //没有点，是文件夹
            filename += i;
        }else {
            String realName = filename.substring(0,lastPointIndex );
            String suffix = filename.substring(lastPointIndex,filename.length());
            filename = realName+i+suffix;
        }
        /**
         * 这里不能省事，确实需要每次都查最新的，因为上次循环可能有新值插入了
         **/
        /*FileEntity fileEntity = this.fileEntityList.get(0);
        FileEntity fileEntity = findByUidAndFilename(filename, p).get(0);
        if (fileEntity != null){
            //已存在的老名与新名重复则继续改名
            if (fileEntity.getFileName().equals(filename)){
                fileRename(filename,++i,p);
            }
        }*/

        //若重复
        if (isExistFilename(filename, p)){
            filename = original; //还原文件名，jdk8以后，相同字符串非同一块地址
            filename = fileRename(filename,++i,p);
        }
        return filename;
    }

    private String fileRename(String filename){
        int lastPointIndex = filename.lastIndexOf(".");
        String realName = filename.substring(0,lastPointIndex);
        String suffix = filename.substring(lastPointIndex,filename.length());
        filename = realName+"random"+new Random().nextInt(10000)+suffix;
        return filename;
    }

    /**
     * 查重入口
     **/
    private String checkRepeat(MyEnum fileType,String filename,String param,String message){
        Integer uid = Integer.valueOf(StpUtil.getLoginId().toString());
        //先检查是否重复，重复才处理
        if (isExistFilename(filename, param)){
            String policy=MyEnum.ORDER.getValue(); //默认策略

            FileEntity instance = this.fileEntityList.get(0); //取出重复的文件

            //查出用户设置
//            Optional<UserSetEntity> optional = setDao.findById(uid);
            if (optional.isPresent()){
                if (MyEnum.FILE == fileType){
                    policy = optional.get().getFileNamePolicy();
                }else if (MyEnum.FLODER == fileType){
                    policy = optional.get().getFloderNamePolicy();
                }
            }else {
                //不存在给当前用户赋默认值
//                setDao.save(new UserSetEntity(uid,"def",policy,policy));
            }

            if (policy.equals(MyEnum.COVER.getValue())){
                //覆盖文件，直接删掉老文件记录，重新生成新文件记录  todo 若是文件夹，也要清理子文件
                fileDao.deleteById(instance.getId());
                return filename;
            }else if (policy.equals(MyEnum.TIME.getValue())){
                //时间戳
                return fileRename(filename,new Date());
            }else if (policy.equals(MyEnum.ORDER.getValue())){
                //序号
                return fileRename(filename, 1,param);
            }else if (policy.equals(MyEnum.RANDOM.getValue())){
                //随机数
                return fileRename(filename);
            }else {
                //弹窗提示
                throw new APIException(AppCode.SaveFAIL,message);
            }
        }
        return filename; //不重复 返回原名
    }


}
